
#include <cstdlib>
#include <iostream>
#include <sstream>
#include <fstream>
#include <string>

#include "../core/tconstraints.h"
#include "mesh.h"

#include <GL/glut.h>
#include "../arcball.h"
#include "../atmatrix.h"
#include "../selection.h"
#include "../glDrawFunctions.h"

//~ #include "../mlsTimes/mlsRaw.h"
//~ #include "../mlsTimes/mlsComparison.h"

using namespace std;

int winw = 700, winh = 700; // window size
int xini, yini; // initial mouse position
int buttonpressed; // 
MeshRenderMode rendermode; // mesh render mode
//~ InteractionMode interactionmode; // interaction mode

Point3 grabStart, grabLast;
int id_moving;

AMatrix<GLfloat>     sceneT;
AMatrix<GLfloat>     sceneIniT;
ArcBall              arcball;
Selection            selection;

typedef TConstraints<Point3>                 PConstraints;
Mesh                                         *mesh=0;
PConstraints                                 constraints;
PConstraints                                 constraintsIni;
vector<Point3>                               positionsIni;

GLuint vbo; /// vertex buffer object id
GLuint nbo; /// normal buffer object id
GLuint ibo; /// element buffer object id

/// time info
float      cfps;
time_t     start, end;
unsigned   frame;
/// Info variables
void* font=GLUT_BITMAP_TIMES_ROMAN_24;
void* fontsmall=GLUT_BITMAP_HELVETICA_12;
string nvertices("Vertices: ");
string nfaces("Faces: ");
string fps("FPS: ");
string options("Options: p/w/h/l/f/s/+/-");
string mode("Mode: Viewing");
string nconstraints("constraints: ");

/// Renders info about the model
void renderInfo();
/// Returns the translation vector in world coordinates of two points in screen space
Vector3 screenToWorld(int xi, int yi, int xe, int ye);
/// Returns the world coordinates of a point in screen space
Point3 screenToWorld(int x, int y);
/// Implements the "zoomin" operation. Narrows the view of the scene.
void zoomIn();
/// Implements the "zoomout" operation. Enlarges the view of the scene.
void zoomOut();

/// Defines the processor used for computing the deformation
char PROCESSOR = 'C'; // C: CPU, V: vertex shader, F: fragment shader
bool flagCtrlPoints = true;
bool flagDistanceField;
int distanceFieldType = 0;

void glDrawModelVertices(Mesh *model) {
   
   unsigned int i=0;
   Vertex_iterator vi = model->vertices_begin();
   while (vi != model->vertices_end()) {
      glPushName(i);
      glBegin(GL_POINTS);
         const Point3 &p = vi->point();
         glVertex3f(p[0], p[1], p[2]);
      glEnd();
      glPopName();
      ++vi; ++i;
   }
}

float dmax=1;
vector<Point3> colores;
void glDrawDistanceField(Mesh *model) {
   
   if (model == 0) return;
   for (Facet_iterator fi = model->facets_begin(); fi != model->facets_end(); ++fi) {
      Halfedge_handle hh = fi->halfedge(), hhbegin = hh;
      glBegin(GL_POLYGON);
      do {
         GLfloat color[4] = {0, 0, 0, 1.0f};
         if (distanceFieldType == 0) {
            float c = hh->vertex()->d/dmax;
            color[0]=1-c; color[1]=0; color[2]=0;
         }
         else if (distanceFieldType == 1) {
            int idx = ((int) (hh->vertex()->d*5))%colores.size();
            color[0]=colores[idx][0]; color[1]=colores[idx][1]; color[2]=colores[idx][2];
         }
         else if (distanceFieldType == 2) {
            int idx = ((int) (hh->vertex()->d*15))%colores.size();
            color[0]=colores[idx][0]; color[1]=colores[idx][1]; color[2]=colores[idx][2];
         }
         
         glMaterialfv(GL_FRONT, GL_DIFFUSE, color);
         const Point3 &p = hh->vertex()->point();
         const Vector3 &n = hh->vertex()->normal();
         glColor4fv(color);
         glNormal3f(n[0], n[1], n[2]);
         glVertex3f(p[0], p[1], p[2]);
         hh = hh->next();
      } while(hh != hhbegin);
      glEnd();
   }
}

void glDrawShortestPaths() {
   
   glPushAttrib(GL_COLOR_BUFFER_BIT | GL_ENABLE_BIT | GL_LINE_BIT);
   glDisable(GL_LIGHTING);
   glEnable(GL_LINE_SMOOTH);
   glDisable(GL_DEPTH_TEST);
   
   glLineWidth(8.0);
   glColor3d(0.0, 0.5, 0.0);
   if (distanceFieldType==2) {
      /// extra line
      Vertex_handle vs = mesh->vertices_begin()[constraints.ids[0]].halfedge()->vertex();
      Vertex_handle vt = mesh->vertices_begin()[constraints.ids[1]].halfedge()->vertex();
      glBegin(GL_LINES);
      const Point3 &ps = vs->point();
      glVertex3f(ps[0], ps[1], ps[2]);
      const Point3 &pt = vt->point();
      glVertex3f(pt[0], pt[1], pt[2]);
      glEnd();
   }
   
   glLineWidth(2.0);
   glColor3d(0.0, 0.0, 0.0);
   for (uint i=0; i<constraints.size()-1; i+=2) {
      int source = constraints.ids[i];
      int target = constraints.ids[i+1];
      Vertex_handle v = mesh->vertices_begin()[target].halfedge()->vertex();
      glBegin(GL_LINE_STRIP);
      const Point3 &p = v->point();
      glVertex3f(p[0], p[1], p[2]+2);
      do {
         v = v->previous();
         const Point3 &p = v->point();
         glVertex3f(p[0], p[1], p[2]+2);
      } while (v->id() != source);
      glEnd();
   }
   
   glPopAttrib();
}

void glDrawConstraints(const TConstraints<Point3> &c) {
   
   glPushAttrib(GL_COLOR_BUFFER_BIT | GL_ENABLE_BIT | GL_POINT_BIT);
   glDisable(GL_LIGHTING);
   glEnable(GL_POINT_SMOOTH);
   glDisable(GL_DEPTH_TEST);
   glColor3d(0.0, 0.0, 1.0);
   glPointSize(14.0);
   
   for (unsigned i=0; i<c.size(); ++i) {
      glPushName(i);
      glBegin(GL_POINTS);
      const Point3 &p = c.getConstraint(i);
      glVertex3f(p[0], p[1], p[2]);
      glEnd();
      glPopName();
   }
   
   glPopAttrib();
   glPointSize(1.0);
}

/// OpenGL display function
void display(void) {
   
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   glOrtho(-1.0,1.0,-1.0,1.0,-4,4);
   
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
   glViewport (0, 0, (GLsizei) winw, (GLsizei) winh);
   
   glMultMatrixf(&sceneT);
   
   /// smooth
   //~ glEnable(GL_LIGHTING);
   //~ glEnable(GL_POLYGON_OFFSET_FILL);
   //~ glPolygonOffset(1,1);
   //~ glDrawElements(GL_TRIANGLES, 3*mesh->nfaces(), GL_UNSIGNED_INT, &mesh->faces[0]);
   //~ glDisable(GL_POLYGON_OFFSET_FILL);
   
   /// lines over the mesh
   //~ glEnable(GL_POLYGON_OFFSET_FILL);
   //~ glPolygonOffset(1,1);
   //~ glDrawElements(GL_TRIANGLES, 3*mesh->nfaces(), GL_UNSIGNED_INT, &mesh->faces[0]);
   //~ glDisable(GL_POLYGON_OFFSET_FILL);
   glColor4f(0.9,0.5,0.0,1.0);
   glPushAttrib(GL_ENABLE_BIT | GL_POLYGON_BIT | GL_LINE_BIT | GL_LIGHTING_BIT);
   glLineWidth(1.0);
   glEnable(GL_CULL_FACE);
   glDisable(GL_LIGHTING);
   glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
   glDrawElements(GL_TRIANGLES, 3*mesh->nfaces(), GL_UNSIGNED_INT, &mesh->faces[0]);
   glPopAttrib();
   
   if (flagDistanceField) {
      //~ glDrawDistanceField(mesh);
      glDrawShortestPaths();
   }
   
   /// draw constraints
   if (flagCtrlPoints) glDrawConstraints(constraints);
   
   /// FPS info
   frame++;
   std::time(&end);
   double ddifftime = std::difftime(end, start);
   if (ddifftime > 1) {
      cfps = frame/ddifftime;
      start = end;
      frame = 0;
      ostringstream s1;
      s1<<cfps;
      fps = "FPS: " + s1.str();
   }
   
   glPushMatrix();
   glLoadIdentity();
   renderInfo();
   glPopMatrix();
   
   glutSwapBuffers();
}

/// Mouse press function, 
void mouseclick(int button, int state, int x, int y) {
   
   id_moving = -1;
   if (state == GLUT_DOWN) {
      xini=x; yini=winh-y;
      buttonpressed = button;
      if (buttonpressed == GLUT_LEFT_BUTTON 
         //and interactionmode == DEFORMATION_MODE
         ) {
         //~ cout<<"buttonpressed == GLUT_LEFT_BUTTON"<<endl;
         
         std::list<unsigned int> selected;
         selection.begin(x, winh-y);
         glDrawConstraints(constraints);
         selection.end(selected);
         if (not selected.empty()) id_moving = selected.front();
         
         grabLast = grabStart = screenToWorld(x, winh-y);
      }
      else if (buttonpressed == GLUT_MIDDLE_BUTTON) {
         arcball.click(xini,yini);
      }
      else if (buttonpressed == GLUT_RIGHT_BUTTON) {
         grabLast = grabStart = screenToWorld(x, winh-y);
      }
      sceneIniT = sceneT;
   }
   else if (state == GLUT_UP) {
      //~ cout<<"buttonpressed:GLUT_UP == GLUT_LEFT_BUTTON"<<endl;
      if (buttonpressed == GLUT_LEFT_BUTTON 
         //and interactionmode == VIEW_MODE
         ) {
         
         y = winh-y;
         std::list<unsigned int> selected;
         if (abs(xini-x)<4 or abs(yini-y)<4) {
            selection.begin(x, y);
            glDrawModelVertices(mesh);
            selection.end(selected);
            // Include the object being altered
            if (not selected.empty()) {
               unsigned idf = selected.front();
               Vertex_iterator vi = mesh->vertices_begin();
               advance(vi, idf);
               
               unsigned idv = vi->id();
               cout<<"idv point: "<<idv<<endl;
               const Point3 p = vi->point();
               constraints.push_back(p, idv);
               
               if (constraints.size() % 2 == 0) {
                  cout<<"computing: "<<endl;
                  //~ flagDistanceField = true;
                  uint s = constraints.size();
                  int source = constraints.ids[s-2];
                  int target = constraints.ids[s-1];
                  mesh->shorestPath_Dijkstra(source, target);
               }
               
               //~ flagDistanceField = true;
               //~ mesh->floodfill_Dijkstra(idv);
               /// looking for the largest distance
               //~ dmax = 0;
               //~ Vertex_iterator vi = mesh->vertices_begin();
               //~ for (unsigned i=0; i<mesh->size_of_vertices(); ++i) {
                  //~ Vertex_handle v = vi[i].halfedge()->vertex();
                  //~ if (v->d > dmax) dmax = v->d;
               //~ }
            }
         }
         
         // label
         ostringstream s2;
         s2<<constraints.size();
         nconstraints = "constraints: " + s2.str();
      }
   }
   glutPostRedisplay();
}

AMatrix<float> mT; /// an auxiliar matrix
/// Mouse move function
void mousemove(int x, int y) {
   
   if (buttonpressed == GLUT_LEFT_BUTTON and 
      //interactionmode == DEFORMATION_MODE and 
      id_moving >= 0) {
      
      Point3 grabEnd = screenToWorld(x, winh-y);
      Vector3 t = grabEnd-grabLast;
      const Point3 &p = constraints.getConstraint(id_moving);
      constraints.setConstraint(id_moving, p + t);
      
      //~ if (PROCESSOR == 'C') { /// CPU
         //~ deformer->execute(constraints);
      //~ }
      //~ else if (PROCESSOR == 'V') { /// GPU: vertex shader
         //~ mlsdeformerVS->execute(constraints);
      //~ }
      //~ else if (PROCESSOR == 'F') { /// GPU: fragment shader
         //~ mlsdeformerFS->execute(constraints);
      //~ }
      
      grabLast = grabEnd;
   }
   else if (buttonpressed == GLUT_MIDDLE_BUTTON) { // rotation handler
      arcball.drag(x, winh-y, &mT);
      sceneT = mT*sceneIniT;
   }
   else if (buttonpressed == GLUT_RIGHT_BUTTON) { // translation handler
      Vector3 t = screenToWorld(xini, yini, x, winh-y);
      
      mT.identity();
      mT.translation(t[0], t[1], t[2]);
      //~ sceneT = mT*sceneIniT;
      sceneT = sceneIniT*mT;
   }
   glutPostRedisplay();
}

vector<int> handlesids; // ids of control points handles
Number MT[4][4];
float angle=0.0;

/// OpenGL key press function
void keypress(unsigned char key, int x, int y) {
   
   switch (key) {
      case 'p':
      case 'P': rendermode = POINTS; break;
      case 'w':
      case 'W': rendermode = WIREFRAME; break;
      case 'h':
      case 'H': rendermode = HIDDENLINES; break;
      case 'l':
      case 'L': rendermode = FLATLINES; break;
      case 'f':
      case 'F': rendermode = FLAT; break;
      case 's':
      case 'S': rendermode = SMOOTH; break;
      case 'c':
      case 'C': flagCtrlPoints = not flagCtrlPoints; break;
      case 'g':
      case 'G': {
         mesh->saveShapeOFF("outputMesh.off");
         break;
      }
      
      case 'd': {
         //~ interactionmode = DEFORMATION_MODE;
         mode = "Mode: Deformation";
         angle = 0;
         constraintsIni = constraints;
         
         flagDistanceField = not flagDistanceField;
         
         break;
      }
      
      case '0': { distanceFieldType = 0;  break; }
      case '1': { distanceFieldType = 1;  break; }
      case '2': { distanceFieldType = 2;  break; }
      
      case '3': { flagDistanceField = not flagDistanceField;  break; }
      
      case '+': { zoomIn(); break; }
      case '-': { zoomOut(); break; }
      case 27: {
         if (mesh!=0) delete mesh; exit(1);
      }
   }
   glutPostRedisplay();
}

/// auxiliar variables
GLdouble mMtx[16], pMtx[16];  /// modelview/projection matrix
GLint viewport[4];            /// the viewport

/// Returns the world coordinates of a point in screen space
Point3 screenToWorld(int x, int y) {
   
   glGetIntegerv(GL_VIEWPORT, viewport);
   glGetDoublev(GL_PROJECTION_MATRIX, pMtx);
   glGetDoublev(GL_MODELVIEW_MATRIX, mMtx);
   
   GLdouble xw, yw, zw;
   gluUnProject(x, y, 1.0, mMtx, pMtx, viewport, &xw, &yw, &zw);
   return Point3(xw, yw, zw);
}

/// Returns the translation vector in world coordinates of two points in screen space
Vector3 screenToWorld(int xi, int yi, int xe, int ye) {
   
   glGetIntegerv(GL_VIEWPORT, viewport);
   glGetDoublev(GL_PROJECTION_MATRIX, pMtx);
   glGetDoublev(GL_MODELVIEW_MATRIX, mMtx);
   
   GLdouble ws[3], we[3];
   gluUnProject(xi, yi, 0, mMtx, pMtx, viewport, &ws[0], &ws[1], &ws[2]);
   gluUnProject(xe, ye, 0, mMtx, pMtx, viewport, &we[0], &we[1], &we[2]);
   
   return Vector3(we[0]-ws[0], we[1]-ws[1], we[2]-ws[2]);
}

/// OpenGL reshape funcion
void reshape (int w, int h) {
   
   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   glOrtho(-1,1,-1,1,-4,4);
   
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
   winw = w; winh = h;
   glViewport (0, 0, (GLsizei) w, (GLsizei) h);
   glutPostRedisplay();
   
   arcball.setBounds(winw, winh);
}

void generateColors() {
   
   colores.push_back(Point3(0.0,0.5,1.0));
   
   colores.push_back(Point3(0.75,0.75,0.0));
   colores.push_back(Point3(0.75,0.0,0.75));
   colores.push_back(Point3(0.5,0.0,0.0));
   colores.push_back(Point3(0.0,1.0,0.5));
   colores.push_back(Point3(0.0,0.0,0.25));
   
   colores.push_back(Point3(0.0,0.3,0.0));
   colores.push_back(Point3(0.7,0.2,0.2));
   colores.push_back(Point3(0.1,0.3,0.3));
   colores.push_back(Point3(0.0,0.2,0.4));
   colores.push_back(Point3(0.5,0.0,1.0));
   
   colores.push_back(Point3(0.4,0.2,0.3));
   colores.push_back(Point3(0.7,0.0,0.0));
   colores.push_back(Point3(0.5,0.5,0.2));
   colores.push_back(Point3(0.25,0.5,0.0));
   colores.push_back(Point3(1.0,0.0,0.5));
   
   colores.push_back(Point3(0.25,0.0,0.0));
   colores.push_back(Point3(1.0,0.5,0.0));
   colores.push_back(Point3(1.0,0.0,0.0));
   colores.push_back(Point3(0.0,1.0,0.0));
   colores.push_back(Point3(0.5,0.0,0.0));
   
   colores.push_back(Point3(0.3,0.3,0.1));
   colores.push_back(Point3(0.6,0.6,0.4));
   
   list<Point3> cores;
   float step=0.2; bool flag = true;
   for (float r=0.0; r<=1.0; r+=step) 
      for (float g=1.0; g>=0.0; g-=step) 
         for (float b=0.0; b<=1.0; b+=step) {
            if (flag) cores.push_back(Point3(r,g,b));
            else      cores.push_front(Point3(b,r,g));
            if (flag) {
               cores.push_back(Point3(r,g,b));
               cores.push_back(Point3(b,r,g));
               cores.push_back(Point3(g,b,r));
            }
            else {
               cores.push_front(Point3(r,g,b));
               cores.push_front(Point3(b,r,g));
               cores.push_front(Point3(g,b,r));
            }
            
            flag = not flag;
         }
   
   colores.insert(colores.end(), cores.begin(), cores.end());
}

/// OpenGL initializations
void init(void) {
   
   /// //////////////////////
   glEnable(GL_NORMALIZE);
   glEnable(GL_DEPTH_TEST);
   glEnable(GL_DITHER);
   glEnable(GL_LIGHTING);
   glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_FALSE);
   
   GLfloat mat_A[] = {0.3, 0.3, 0.3, 1.0};
   GLfloat mat_D[] = {1.0, 0.4, 0.0, 1.0};
   GLfloat mat_S[] = {1.0, 1.0, 1.0, 1.0};
   GLfloat shine[] = {128.0};
   glMaterialfv(GL_FRONT, GL_AMBIENT,   mat_A);
   glMaterialfv(GL_FRONT, GL_DIFFUSE,   mat_D);
   //~ glMaterialfv(GL_FRONT, GL_SPECULAR,  mat_S);
   glMaterialfv(GL_FRONT, GL_SHININESS, shine);
   
   GLfloat pos_L0[] = { 10.0, 10.0, 0.0, 0.0};
   GLfloat pos_L1[] = {-10.0, 10.0, 0.0, 0.0};
   GLfloat pos_L2[] = { 0.0, 0.0, 10.0, 0.0};
   GLfloat col_2[]  = { 1.0, 1.0, 1.0, 1.0};
   glEnable(GL_LIGHT0);
   glEnable(GL_LIGHT1);
   glEnable(GL_LIGHT2);
   glLightfv(GL_LIGHT0, GL_POSITION, pos_L0);
   glLightfv(GL_LIGHT1, GL_POSITION, pos_L1);
   glLightfv(GL_LIGHT2, GL_POSITION, pos_L2);
   glLightfv(GL_LIGHT2, GL_DIFFUSE,  col_2);
   glLightfv(GL_LIGHT2, GL_SPECULAR, col_2);
   /// 
   
   glClearColor(1,1,1,1);
   glColor4f(1,1,0,1);
   rendermode = SMOOTH;
   //~ interactionmode = VIEW_MODE;
   
   arcball.setBounds(winw, winh);
   sceneT.identity();
   
   std::time(&start);
   frame = 0;
   
   glEnableClientState(GL_VERTEX_ARRAY);
   glEnableClientState(GL_NORMAL_ARRAY);
   typedef Mesh::Vertex Vertex;
   Vertex_iterator vi = mesh->vertices_begin();
   glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &(vi->point()[0]));
   glNormalPointer(GL_FLOAT, sizeof(Vertex), &(vi->normal()[0]));
   
   generateColors();
}

/// Implements the "zoomin" operation. Narrows the view of the scene.
void zoomIn() {
   
   AMatrix<float> mT;
   mT.identity();
   mT.scaling(1.2);
   sceneT = mT*sceneT;
}

/// Implements the "zoomout" operation. Enlarges the view of the scene.
void zoomOut() {
   
   AMatrix<float> mT;
   mT.identity();
   mT.scaling(1.0/1.2);
   sceneT = mT*sceneT;
}

/// Renders a string
void renderBitmapString(float x, float y, float z, void *font, const char *string) {
   const char *c;
   glRasterPos3f(x, y,z);
   for (c=string; *c != '\0'; c++) {
      glutBitmapCharacter(font, *c);
   }
}

/// Renders info about the model
void renderInfo() {
   
   glDisable(GL_LIGHTING); glDisable(GL_DEPTH_TEST);
   glColor4f(0,0,0,1);
   renderBitmapString(-0.9,0.90,0,(void *)fontsmall, nvertices.c_str());
   renderBitmapString(-0.9,0.85,0,(void *)fontsmall, nfaces.c_str());
   renderBitmapString(-0.9,0.80,0,(void *)fontsmall, fps.c_str());
   renderBitmapString(-0.9,0.75,0,(void *)fontsmall, options.c_str());
   renderBitmapString(-0.9,0.65,0,(void *)fontsmall, nconstraints.c_str());
   glEnable(GL_LIGHTING); glEnable(GL_DEPTH_TEST);
}

void printInfoAndExit() {
   
   cout<<endl;
   cout<<"Usage: meshdef filename.off"<<endl;
   exit(0);
}

void open_mesh(string filename) {
   
   string extensao =filename.substr(filename.size()-3,filename.size());
   if (extensao == "off") {
      mesh = Mesh::load_off(filename.c_str());
      ostringstream s1, s2;
      s1<<mesh->nvertices();
      s2<<mesh->nfaces();
      nvertices += s1.str();
      nfaces += s2.str();
   }
   
   ostringstream s2;
   s2<<constraints.size();
   nconstraints = "constraints: " + s2.str();
   
   //~ {
   //~ Vertex_iterator vi = mesh->vertices_begin();
   //~ constraints.push_back(vi[43].point(), 43);
   //~ constraints.push_back(vi[71].point(), 71);
   //~ uint s = constraints.size();
   //~ int source = constraints.ids[s-2];
   //~ int target = constraints.ids[s-1];
   //~ mesh->shorestPath_Dijkstra(source, target);
   //~ }
   
   assert(mesh != 0);
}

/// Process the program's parameters
void input(int argc, char **argv) {
   
   int nargs = argc; // 
   if (nargs<2 or nargs>2) printInfoAndExit();
   
   open_mesh(argv[1]);
}

int main(int argc, char **argv) {
   
   // Process the input parameters
   input(argc, argv);
   
   // Glut setup
   glutInit(&argc, argv);
   glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
   glutInitWindowSize(winw, winh);
   glutInitWindowPosition(150, 0);
   glutCreateWindow("MLS mesh deformation prototype");
   
   // OpenGL and data initialization
   init();
   
   // Callbacks registration
   glutDisplayFunc(display);
   glutReshapeFunc(reshape);
   glutMouseFunc(mouseclick);
   glutMotionFunc(mousemove);
   glutKeyboardFunc(keypress);
   glutMainLoop();
   
   return 0;
}
